﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using NthDownload.Agent.EventArguments;
using NthDownload.Agent.Exceptions;
using NthDownload.Common.Plugins;
using NthDownload.Contracts;
using System.Threading.Tasks;
using NthDownload.Contracts.Plugins.Downloads;
using NthDownload.Contracts.Plugins.BandwidthProviders;

namespace NthDownload.Agent
{
    public class DownloadQueue : INotifyPropertyChanged
    {
        #region Events

        #region DownloadStarted

        /// <summary>
        /// Occurs when queue starts a download
        /// </summary>
        public event EventHandler<DownloadOperationCompletedEventArgs> DownloadStarted;

        /// <summary>
        /// Invokes the DownloadStarted event
        /// </summary>
        protected void OnDownloadStarted(IDownload download, object stateObject)
        {
            if (DownloadStarted != null)
            {
                DownloadStarted.Invoke(this, new DownloadOperationCompletedEventArgs(download, stateObject, null, false, false));
            }
        }

        #endregion DownloadStarted

        #region DownloadPaused

        /// <summary>
        /// Occurs when the queue pauses a download
        /// </summary>
        public event EventHandler<DownloadOperationCompletedEventArgs> DownloadPaused;

        /// <summary>
        /// Invokes the DownloadPaused event
        /// </summary>
        protected void OnDownloadPaused(IDownload download, object stateObject)
        {
            if (DownloadPaused != null)
            {
                DownloadPaused.Invoke(this, new DownloadOperationCompletedEventArgs(download, stateObject, null, false, false));
            }
        }

        #endregion DownloadPaused

        #region DownloadStopped

        /// <summary>
        /// Occurs when the queue stops a download
        /// </summary>
        public event EventHandler<DownloadOperationCompletedEventArgs> DownloadStopped;

        /// <summary>
        /// Invokes the DownloadStopped event
        /// </summary>
        protected void OnDownloadStopped(IDownload download, object stateObject, Exception error, bool aborted, bool canceled)
        {
            if (DownloadStopped != null)
            {
                DownloadStopped.Invoke(this, new DownloadOperationCompletedEventArgs(download, stateObject, error, aborted, canceled));
            }
        }

        #endregion DownloadStopped

        #region DownloadFinished

        /// <summary>
        /// Occurs when the queue finishes a download
        /// </summary>
        public event EventHandler<DownloadOperationCompletedEventArgs> DownloadFinished;

        /// <summary>
        /// Invokes the DownloadFinished event
        /// </summary>
        protected void OnDownloadFinished(IDownload download, object stateObject)
        {
            if (DownloadFinished != null)
            {
                DownloadFinished.Invoke(this, new DownloadOperationCompletedEventArgs(download, stateObject, null, false, false));
            }
        }

        #endregion DownloadFinished

        #region INotifyPropertyChanged

        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        #endregion INotifyPropertyChanged

        #endregion Events

        #region Class Members

        private IBandwidthProvider _bandwidthProvider;
        private int _maxRunningDownloads;
        private int _maxRetries;
        private int _maxThreads;
        private bool _startDownloadsImmediately;

        private readonly object _operationSyncObject = new object();

        #endregion Class Members

        #region Constructor

        public DownloadQueue()
            : this(null)
        {
        }

        public DownloadQueue(IBandwidthProvider bandwidthProvider)
        {
            Downloads = new Dictionary<string, IDownload>();
            RunningDownloads = new Dictionary<string, DownloadProcessor>();
            MaxRunningDownloads = 1;
            MaxRetries = 1;
            MaxThreads = 1;
            BandwidthProvider = bandwidthProvider;

            Operations = new Dictionary<TaskCompletionSource<QueueOperation>, QueueOperation>();
        }

        #endregion Constructor

        #region Properties

        /// <summary>
        /// The bandwidth provider to draw bandwidth from
        /// </summary>
        public IBandwidthProvider BandwidthProvider
        {
            get
            {
                return _bandwidthProvider;
            }
            set
            {
                _bandwidthProvider = value;
                OnPropertyChanged("BandwidthProvider");
                UpdateBandwidthProviders();
            }
        }

        /// <summary>
        /// The downloads in the queue. This collection is indexed by the
        /// download GUID.
        /// </summary>
        public Dictionary<string, IDownload> Downloads { get; private set; }

        /// <summary>
        /// The downloads that are currently being processed. This collection
        /// is indexed by the Download GUD with the value being the download processor
        /// which is working on the download.
        /// </summary>
        public Dictionary<string, DownloadProcessor> RunningDownloads { get; private set; }

        /// <summary>
        /// The maximum number of downloads that can be runnings at any 
        /// one time in this queue. If the queue is active when this value 
        /// is changed it'll attempt to balance the running downloads to match
        /// the max number of running downloads.
        /// </summary>
        public int MaxRunningDownloads
        {
            get
            {
                return Math.Max(1, _maxRunningDownloads);
            }
            set
            {
                _maxRunningDownloads = Math.Max(1, value);
                OnPropertyChanged("MaxRunningDownloads");
                SequenceQueuedDownloads();
            }
        }

        /// <summary>
        /// The maximum number of retries downloads in this queue can
        /// encounter before stopping. If this value is changed the queue
        /// will attempt to change the value for all downloads in the queue.
        /// If this value is changed the queue will attempt to start/queue 
        /// downloads accordingly.
        /// </summary>
        public int MaxRetries
        {
            get
            {
                return Math.Max(1, _maxRetries);
            }
            set
            {
                _maxRetries = Math.Max(1, value);
                OnPropertyChanged("MaxRetries");
                UpdateMaxRetries();
            }
        }

        /// <summary>
        /// The maximum number of threads downloads in this queue can
        /// use to download on. If this value is change the queue
        /// will attempt to change the value for all downloads in the queue.
        /// </summary>
        public int MaxThreads
        {
            get
            {
                return Math.Max(1, _maxThreads);
            }
            set
            {
                _maxThreads = Math.Max(1, value);
                OnPropertyChanged("MaxThreads");
                UpdateMaxThreads();
            }
        }

        /// <summary>
        /// Indicates if the queue should attempt to start downloads added 
        /// automatically.
        /// </summary>
        public bool StartDownloadsImmediately
        {
            get
            {
                return _startDownloadsImmediately;
            }
            set
            {
                _startDownloadsImmediately = value;
                OnPropertyChanged("StartDownloadsImmediately");
            }
        }

        #endregion Properties

        #region Private Properties

        /// <summary>
        /// The operations that are currently being executed
        /// </summary>
        private Dictionary<TaskCompletionSource<QueueOperation>, QueueOperation> Operations { get; set; }

        #endregion Private Properties

        #region Methods

        /// <summary>
        /// Add downloads to the queue.
        /// </summary>
        /// <param name="downloads">The downloads to add to the queue.</param>
        /// <param name="startTask">The Task for the start operation the is performed if startg downloads immediately is enabled.</param>
        public Task<QueueOperation> AddDownloads(IEnumerable<IDownload> downloads, out Task<QueueOperation> startTask)
        {
            startTask = null;

            //
            // Get downloads that aren't already in the queue
            //
            IDownload testDownload;
            IEnumerable<IDownload> downloadsToAdd = downloads.Where(d => !Downloads.TryGetValue(d.GUID, out testDownload));

            //
            // Start operaton
            //
            TaskCompletionSource<QueueOperation> startTaskCompletionSource = new TaskCompletionSource<QueueOperation>();
            StartOperation(startTaskCompletionSource, downloadsToAdd.Count());

            if (startTaskCompletionSource.Task.IsFaulted)
            {
                return startTaskCompletionSource.Task;
            }

            //
            // If there are no downloads to add end operation
            //
            if (!downloadsToAdd.Any())
            {
                EndOperation(startTaskCompletionSource);
                return startTaskCompletionSource.Task;
            }

            //
            // Add downloads
            //
            foreach (IDownload download in downloadsToAdd)
            {
                AddDownloadToQueue(download);
                UpdateOperation(startTaskCompletionSource, download, null, false, false);

                if (startTaskCompletionSource.Task.IsFaulted)
                {
                    return startTaskCompletionSource.Task;
                }
            }

            //
            // If start immediately is enabled start the downloads
            //
            if (StartDownloadsImmediately)
            {
                startTask = StartDownloads(downloadsToAdd.Select(d => d.GUID));
            }

            return startTaskCompletionSource.Task;
        }

        /// <summary>
        /// Removes downloads from the queue.
        /// </summary>
        /// <param name="downloads">The downloads to remove</param>
        public Task<QueueOperation> RemoveDownloads(IEnumerable<IDownload> downloads)
        {
            return RemoveDownloads(downloads.Select(d => d.GUID));
        }

        /// <summary>
        /// Removes downloads from the queue.
        /// </summary>
        /// <param name="guids">The GUIDs of the downloads to remove.</param>.
        public Task<QueueOperation> RemoveDownloads(IEnumerable<string> guids)
        {
            //
            // Get downloads to remove
            //
            List<IDownload> downloadsToRemove = new List<IDownload>();
            foreach (string guid in guids)
            {
                IDownload testDownload;
                if (Downloads.TryGetValue(guid, out testDownload))
                {
                    downloadsToRemove.Add(testDownload);
                }
            }

            //
            // Start operaton with an extra action. The extra action
            // is used is there are no downloads to remove or for the sort action at the end
            //
            TaskCompletionSource<QueueOperation> taskCompletionSource = new TaskCompletionSource<QueueOperation>();
            int actionCount = downloadsToRemove.Count + 1;
            StartOperation(taskCompletionSource, actionCount);

            if (taskCompletionSource.Task.IsFaulted)
            {
                return taskCompletionSource.Task;
            }

            //
            // If there are no downloads to remove end operation
            //
            if (downloadsToRemove.Count == 0)
            {
                UpdateOperation(taskCompletionSource);
                return taskCompletionSource.Task;
            }

            //
            // Remove downloads
            //
            foreach (IDownload download in downloadsToRemove)
            {
                //
                // Check if the download is running
                //
                DownloadProcessor downloadProcessor;
                if (RunningDownloads.TryGetValue(download.GUID, out downloadProcessor))
                {
                    DownloadRunningException error = new DownloadRunningException("Can't remove a download while it is being processed.");
                    UpdateOperation(taskCompletionSource, downloadProcessor.Download, error, false, false);

                    if (taskCompletionSource.Task.IsFaulted)
                    {
                        return taskCompletionSource.Task;
                    }

                    continue;
                }

                //
                // Remove download from the queue
                //
                Downloads.Remove(download.GUID);
                UpdateOperation(taskCompletionSource, download, null, false, false);

                if (taskCompletionSource.Task.IsFaulted)
                {
                    return taskCompletionSource.Task;
                }
            }

            //
            // Reorder downloads
            //
            IEnumerable<IDownload> orderedDownloads = Downloads.Values.OrderBy(d => d.Position);

            int i = 0;
            foreach (IDownload download in orderedDownloads)
            {
                download.Position = i;
                i++;
            }

            UpdateOperation(taskCompletionSource);
            return taskCompletionSource.Task;
        }

        /// <summary>
        /// Starts processing given downloads. If any of the provided downloads don't belong 
        /// to the queue they're added. If the max number of running downloads has
        /// been reached downloads are queued.
        /// </summary>
        /// <param name="downloads">The downloads to process.</param>
        public Task<QueueOperation> StartDownloads(IEnumerable<IDownload> downloads)
        {
            //
            // Add downloads to the queue that don't exist
            //
            foreach (IDownload download in downloads)
	        {
                IDownload testDownload;
                if (!Downloads.TryGetValue(download.GUID, out testDownload))
                {
                    AddDownloadToQueue(download);
                }		 
	        }

            //
            // Start downloads
            //
            return StartDownloads(downloads.Select(d => d.GUID));
        }

        /// <summary>
        /// Starts processing given downloads. If the max number of running downloads is
        /// reached downloads are queued.
        /// </summary>
        /// <param name="guids">The GUIDs of the downloads to start.</param>
        public Task<QueueOperation> StartDownloads(IEnumerable<string> guids)
        {
            //
            // Get downloads from GUIDs
            //
            List<IDownload> downloadsToSatrt = new List<IDownload>();
            foreach (string guid in guids)
            {
                IDownload download;
                if (Downloads.TryGetValue(guid, out download))
                {
                    downloadsToSatrt.Add(download);
                }
            }

            //
            // Start operation
            //
            TaskCompletionSource<QueueOperation> taskCompletionSource = new TaskCompletionSource<QueueOperation>();
            StartOperation(taskCompletionSource, downloadsToSatrt.Count);

            if (taskCompletionSource.Task.IsFaulted)
            {
                return taskCompletionSource.Task;
            }

            //
            // If there are no downloads to start end operation
            //
            if (downloadsToSatrt.Count == 0)
            {
                EndOperation(taskCompletionSource);
                return taskCompletionSource.Task;
            }

            //
            // Start downloads
            //
            foreach (IDownload download in downloadsToSatrt)
            {
                //
                // Check if the download is already running
                //
                DownloadProcessor downloadProcessor;
                if (RunningDownloads.TryGetValue(download.GUID, out downloadProcessor))
                {
                    DownloadRunningException error = new DownloadRunningException("Download is already being processed.");
                    UpdateOperation(taskCompletionSource, download, error, false, false);

                    if (taskCompletionSource.Task.IsFaulted)
                    {
                        return taskCompletionSource.Task;
                    }

                    continue;
                }

                //
                // Fix info on the data that has already been downloaded
                //
                download.ResetDownloadedInfo(download.Status == DownloadStatuses.Finished);

                //
                // Fix any invalid statuses
                //
                download.FixStatus(DownloadStatuses.Queued, DownloadStatuses.Queued, DownloadStatuses.Queued);

                //
                // If there are already the max number of downloads running then queue the download
                //
                if (RunningDownloads.Count >= MaxRunningDownloads)
                {
                    download.Status = DownloadStatuses.Queued;
                    UpdateOperation(taskCompletionSource, download, null, false, false);

                    if (taskCompletionSource.Task.IsFaulted)
                    {
                        return taskCompletionSource.Task;
                    }

                    continue;
                }

                //
                // Create the download processor, add to running downloads
                // and subscribe to events.
                //
                DownloadProcessor processor = new DownloadProcessor(download, BandwidthProvider, MaxThreads, MaxRetries);
                RunningDownloads.Add(download.GUID, processor);
                SubscribeToDownloadProcessorEvents(processor);

                //
                // Try start the download. If and exception occurs while starting, it
                // should be removed from running downloads the operation updated
                //
                try
                {
                    processor.Start(taskCompletionSource);
                }
                catch (Exception e)
                {
                    //
                    // If the exception is unknown, then put the download in an erroneous state & remove
                    // from running downloads
                    //
                    download.Status = DownloadStatuses.Error;
                    RunningDownloads.Remove(download.GUID);

                    Exception error = new Exception("An error was encountered while attempting to start the download", e);
                    UpdateOperation(taskCompletionSource, download, error, false, false);
                }
            }

            return taskCompletionSource.Task;
        }

        /// <summary>
        /// Starts downloads in the queue until the max number of running downloads 
        /// is reached. This method only starts downloads which are ready to start, 
        /// it will not act upon the downloads which aren't ready to start.
        /// A download is ready to start when it isn't running and it's status
        /// is Queued or Downloading.
        /// </summary>
        public Task<QueueOperation> StartQueuedDownloads()
        {
            //
            // If the maximum number of downloads are already running exit
            //
            if (RunningDownloads.Count >= MaxRunningDownloads)
            {
                TaskCompletionSource<QueueOperation> taskCompletionSource = new TaskCompletionSource<QueueOperation>();
                StartOperation(taskCompletionSource, 0);
                EndOperation(taskCompletionSource);
                return taskCompletionSource.Task;
            }

            //
            // Get the downloads that are available to start
            //
            DownloadProcessor tmpDownloadProcessor;
            IEnumerable<IDownload> availableDownloads = Downloads.Values.Where(d => !RunningDownloads.TryGetValue(d.GUID, out tmpDownloadProcessor) && (d.Status == DownloadStatuses.Downloading || d.Status == DownloadStatuses.Queued)).OrderBy(d => d.Position);
            
            //
            // Start downloads for the number of available slots
            //
            return StartDownloads(availableDownloads.Take(MaxRunningDownloads - RunningDownloads.Count));
        }

        /// <summary>
        /// Pause the given downloads
        /// </summary>
        /// <param name="downloads">The downloads to pause.</param>
        public Task<QueueOperation> PauseDownloads(IEnumerable<IDownload> downloads)
        {
            return PauseDownloads(downloads.Select(d => d.GUID));
        }

        /// <summary>
        /// Pause the given downloads
        /// </summary>
        /// <param name="guids">GUIDs of the downloads to pause.</param>
        public Task<QueueOperation> PauseDownloads(IEnumerable<string> guids)
        {
            //
            // Get downloads to pause
            //
            List<IDownload> downloadsToPause = new List<IDownload>();
            foreach (string guid in guids)
            {
                IDownload testDownload;
                if (Downloads.TryGetValue(guid, out testDownload))
                {
                    downloadsToPause.Add(testDownload);
                }
            }

            //
            // Start operaton
            //
            TaskCompletionSource<QueueOperation> taskCompletionSource = new TaskCompletionSource<QueueOperation>();
            StartOperation(taskCompletionSource, downloadsToPause.Count);

            if (taskCompletionSource.Task.IsFaulted)
            {
                return taskCompletionSource.Task;
            }

            //
            // If there are no downloads to start end operation
            //
            if (downloadsToPause.Count == 0)
            {
                EndOperation(taskCompletionSource);
                return taskCompletionSource.Task;
            }

            //
            // Pause Downloads
            //
            List<DownloadProcessor> downloadProcessorsToPause = new List<DownloadProcessor>();
            foreach (IDownload download in downloadsToPause)
            {
                //
                // If download isn't running
                //
                DownloadProcessor processor;
                if (!RunningDownloads.TryGetValue(download.GUID, out processor))
                {
                    //
                    // Check the download is in a valid state
                    //
                    if (download.Status != DownloadStatuses.Downloading && download.Status != DownloadStatuses.Queued && download.Status != DownloadStatuses.MassPausePending)
                    {
                        InvalidStatusException error = new InvalidStatusException("Invalid status, download must be Queued, MassPausePending or Downloading.");
                        UpdateOperation(taskCompletionSource, download, error, false, false);

                        if (taskCompletionSource.Task.IsFaulted)
                        {
                            return taskCompletionSource.Task;
                        }

                        continue;
                    }

                    //
                    // Update the status of the downlaod to be paused.
                    //
                    download.Status = DownloadStatuses.Paused;

                    //
                    // Update operation and continue to next element
                    //
                    UpdateOperation(taskCompletionSource, download, null, false, false);

                    if (taskCompletionSource.Task.IsFaulted)
                    {
                        return taskCompletionSource.Task;
                    }

                    continue;
                }

                //
                // Set status to MassPausePending and add to downloadProcessorsToPause
                //
                download.Status = DownloadStatuses.MassPausePending;
                downloadProcessorsToPause.Add(processor);
            }

            //
            // Pause download processors
            //
            foreach (DownloadProcessor downloadProcessor in downloadProcessorsToPause)
            {
                try
                {
                    downloadProcessor.Pause(taskCompletionSource);
                }
                catch (OperationPendingException operationPendingException)
                {
                    UpdateOperation(taskCompletionSource, downloadProcessor.Download, operationPendingException, false, false);
                }
                catch (InvalidStatusException invalidStatusException)
                {
                    UpdateOperation(taskCompletionSource, downloadProcessor.Download, invalidStatusException, false, false);
                }
                catch (Exception e)
                {
                    Exception unknownException = new Exception("An unknown exception occured while attempting to pause the download.", e);
                    UpdateOperation(taskCompletionSource, downloadProcessor.Download, unknownException, false, false);
                }
            }

            return taskCompletionSource.Task;
        }

        /// <summary>
        /// Stop the given downloads
        /// </summary>
        /// <param name="downloads">The downloads to stop.</param>
        public Task<QueueOperation> StopDownloads(IEnumerable<IDownload> downloads)
        {
            return StopDownloads(downloads.Select(d => d.GUID));
        }

        /// <summary>
        /// Stop the given downloads
        /// </summary>
        /// <param name="guids">GUIDs of the downloads to stop.</param>
        public Task<QueueOperation> StopDownloads(IEnumerable<string> guids)
        {
            //
            // Get downloads to stop
            //
            List<IDownload> downloadsToStop = new List<IDownload>();
            foreach (string guid in guids)
            {
                IDownload testDownload;
                if (Downloads.TryGetValue(guid, out testDownload))
                {
                    downloadsToStop.Add(testDownload);
                }
            }

            //
            // Start operaton
            //
            TaskCompletionSource<QueueOperation> taskCompletionSource = new TaskCompletionSource<QueueOperation>();
            StartOperation(taskCompletionSource, downloadsToStop.Count);

            if (taskCompletionSource.Task.IsFaulted)
            {
                return taskCompletionSource.Task;
            }

            //
            // If there are no downloads to start end operation
            //
            if (downloadsToStop.Count == 0)
            {
                EndOperation(taskCompletionSource);
                return taskCompletionSource.Task;
            }

            //
            // Stop Downloads
            //
            List<DownloadProcessor> downloadProcessorsToStop = new List<DownloadProcessor>();
            foreach (IDownload download in downloadsToStop)
            {
                //
                // If download isn't running
                //
                DownloadProcessor processor;
                if (!RunningDownloads.TryGetValue(download.GUID, out processor))
                {
                    //
                    // Check the download is in a valid state
                    //
                    if (download.Status != DownloadStatuses.Paused && download.Status != DownloadStatuses.Downloading && download.Status != DownloadStatuses.Queued && download.Status != DownloadStatuses.MassStopPending)
                    {
                        InvalidStatusException error = new InvalidStatusException("Invalid status, download must be Queued, MassPausePending or Downloading.");
                        UpdateOperation(taskCompletionSource, download, error, false, false);

                        if (taskCompletionSource.Task.IsFaulted)
                        {
                            return taskCompletionSource.Task;
                        }

                        continue;
                    }

                    //
                    // Update the status of the downlaod to be paused.
                    //
                    download.Status = DownloadStatuses.Stopped;

                    //
                    // Update operation and continue to next element
                    //
                    UpdateOperation(taskCompletionSource, download, null, false, false);

                    if (taskCompletionSource.Task.IsFaulted)
                    {
                        return taskCompletionSource.Task;
                    }

                    continue;
                }

                //
                // Set status to MassPausePending and add to downloadProcessorsToPause
                //
                download.Status = DownloadStatuses.MassStopPending;
                downloadProcessorsToStop.Add(processor);
            }

            //
            // Stop download processors
            //
            foreach (DownloadProcessor downloadProcessor in downloadProcessorsToStop)
            {
                try
                {
                    downloadProcessor.Stop(taskCompletionSource);
                }
                catch (OperationPendingException operationPendingException)
                {
                    UpdateOperation(taskCompletionSource, downloadProcessor.Download, operationPendingException, false, false);
                }
                catch (InvalidStatusException invalidStatusException)
                {
                    UpdateOperation(taskCompletionSource, downloadProcessor.Download, invalidStatusException, false, false);
                }
                catch (Exception e)
                {
                    Exception unknownException = new Exception("An unknown exception occured while attempting to stop the download.", e);
                    UpdateOperation(taskCompletionSource, downloadProcessor.Download, unknownException, false, false);
                }
            }

            return taskCompletionSource.Task;
        }

        /// <summary>
        /// Cancel the given downloads
        /// </summary>
        /// <param name="downloads">The downloads to cancel.</param>
        public Task<QueueOperation> CancelDownloads(IEnumerable<IDownload> downloads)
        {
            return CancelDownloads(downloads.Select(d => d.GUID));
        }

        /// <summary>
        /// Cancel the given downloads
        /// </summary>
        /// <param name="guids">GUIDs of the downloads to cancel.</param>
        public Task<QueueOperation> CancelDownloads(IEnumerable<string> guids)
        {
            //
            // Get downloads to stop
            //
            List<IDownload> downloadsToCancel = new List<IDownload>();
            foreach (string guid in guids)
            {
                IDownload testDownload;
                if (Downloads.TryGetValue(guid, out testDownload))
                {
                    downloadsToCancel.Add(testDownload);
                }
            }

            //
            // Start operaton
            //
            TaskCompletionSource<QueueOperation> taskCompletionSource = new TaskCompletionSource<QueueOperation>();
            StartOperation(taskCompletionSource, downloadsToCancel.Count);

            if (taskCompletionSource.Task.IsFaulted)
            {
                return taskCompletionSource.Task;
            }

            //
            // If there are no downloads to start end operation
            //
            if (downloadsToCancel.Count == 0)
            {
                EndOperation(taskCompletionSource);
                return taskCompletionSource.Task;
            }

            //
            // Cancel Downloads
            //
            List<DownloadProcessor> downloadProcessorsToCancel = new List<DownloadProcessor>();
            foreach (IDownload download in downloadsToCancel)
            {
                //
                // If download isn't running
                //
                DownloadProcessor processor;
                if (!RunningDownloads.TryGetValue(download.GUID, out processor))
                {
                    //
                    // Check the download is in a valid state
                    //
                    if (download.Status != DownloadStatuses.Paused && download.Status != DownloadStatuses.Downloading && download.Status != DownloadStatuses.Queued && download.Status != DownloadStatuses.Stopped && download.Status != DownloadStatuses.MassCancelPending)
                    {
                        InvalidStatusException error = new InvalidStatusException("Invalid status, download must be Paused, Queued, Downloading, MassCancelPending or Stopped.");
                        UpdateOperation(taskCompletionSource, download, error, false, false);

                        if (taskCompletionSource.Task.IsFaulted)
                        {
                            return taskCompletionSource.Task;
                        }

                        continue;
                    }

                    //
                    // Update the status of the downlaod to be paused.
                    //
                    download.Status = DownloadStatuses.Canceled;

                    //
                    // Update operation and continue to next element
                    //
                    UpdateOperation(taskCompletionSource, download, null, false, false);

                    if (taskCompletionSource.Task.IsFaulted)
                    {
                        return taskCompletionSource.Task;
                    }

                    continue;
                }

                //
                // Set status to MassPausePending and add to downloadProcessorsToPause
                //
                download.Status = DownloadStatuses.MassCancelPending;
                downloadProcessorsToCancel.Add(processor);
            }

            //
            // Cancel download processors
            //
            foreach (DownloadProcessor downloadProcessor in downloadProcessorsToCancel)
            {
                try
                {
                    downloadProcessor.Cancel(taskCompletionSource);
                }
                catch (OperationPendingException operationPendingException)
                {
                    UpdateOperation(taskCompletionSource, downloadProcessor.Download, operationPendingException, false, false);
                }
                catch (InvalidStatusException invalidStatusException)
                {
                    UpdateOperation(taskCompletionSource, downloadProcessor.Download, invalidStatusException, false, false);
                }
                catch (Exception e)
                {
                    Exception unknownException = new Exception("An unknown exception occured while attempting to stop the download.", e);
                    UpdateOperation(taskCompletionSource, downloadProcessor.Download, unknownException, false, false);
                }
            }

            return taskCompletionSource.Task;
        }

        /// <summary>
        /// Moves the givien downloads up in the queue
        /// </summary>
        /// <param name="downloads">The downloads to move up in the queue</param>
        public Task<QueueOperation> MoveDownloadsUp(IEnumerable<IDownload> downloads)
        {
            return MoveDownloadsUp(downloads.Select(d => d.GUID));
        }

        /// <summary>
        /// Moves the givien downloads up in the queue
        /// </summary>
        /// <param name="guids">GUIDs of the downloads to move up in the queue</param>
        public Task<QueueOperation> MoveDownloadsUp(IEnumerable<string> guids)
        {
            //
            // Get downloads to move
            //
            List<IDownload> downloadsToMove = new List<IDownload>();
            foreach (string guid in guids)
            {
                IDownload testDownload;
                if (Downloads.TryGetValue(guid, out testDownload))
                {
                    downloadsToMove.Add(testDownload);
                }
            }

            Dictionary<string, IDownload> sortedDownloadsToMove = downloadsToMove.OrderBy(d => d.Position).ToDictionary(download => download.GUID);

            //
            // Start operaton
            //
            TaskCompletionSource<QueueOperation> taskCompletionSource = new TaskCompletionSource<QueueOperation>();
            StartOperation(taskCompletionSource, downloadsToMove.Count);

            if (taskCompletionSource.Task.IsFaulted)
            {
                return taskCompletionSource.Task;
            }

            //
            // If there are no downloads to move end operation
            //
            if (downloadsToMove.Count == 0)
            {
                EndOperation(taskCompletionSource);
                return taskCompletionSource.Task;
            }

            //
            // Sort downloads into a list
            //
            List<IDownload> sortedDownloads = Downloads.Values.OrderBy(d => d.Position).ToList();

            //
            // Move downloads
            //
            foreach (IDownload download in sortedDownloadsToMove.Values)
            {
                //
                // Get downloads index in the sortedDownloads collection 
                //
                int i = sortedDownloads.IndexOf(download);

                //
                // Continue to next iteration if element is already at the top or if the download wasn't found
                //
                if (i <= 0 || i > sortedDownloads.Count - 1)
                {
                    continue;
                }

                //
                // Swap indexes and position properties with element above the current element
                // if the element above the current element is an element that also needs to be
                // moved up then move to next iteration
                //
                IDownload higherDownload = sortedDownloads[i - 1];

                IDownload testDownload;
                if (sortedDownloadsToMove.TryGetValue(higherDownload.GUID, out testDownload))
                {
                    continue;
                }

                sortedDownloads[i - 1] = download;
                sortedDownloads[i] = higherDownload;

                int higherPosition = higherDownload.Position;
                higherDownload.Position = download.Position;
                download.Position = higherPosition;

                //
                // Update operation for current download
                //
                UpdateOperation(taskCompletionSource, download, null, false, false);

                if (taskCompletionSource.Task.IsFaulted)
                {
                    return taskCompletionSource.Task;
                }
            }

            return taskCompletionSource.Task;
        }

        /// <summary>
        /// Moves the givien downloads down in the queue
        /// </summary>
        /// <param name="downloads">The downloads to move down in the queue</param>
        public Task<QueueOperation> MoveDownloadsDown(IEnumerable<IDownload> downloads)
        {
            return MoveDownloadsDown(downloads.Select(d => d.GUID));
        }

        /// <summary>
        /// Moves the givien downloads down in the queue
        /// </summary>
        /// <param name="guids">GUIDs of the downloads to move down in the queue</param>
        public Task<QueueOperation> MoveDownloadsDown(IEnumerable<string> guids)
        {
            //
            // Get downloads to move
            //
            List<IDownload> downloadsToMove = new List<IDownload>();
            foreach (string guid in guids)
            {
                IDownload testDownload;
                if (Downloads.TryGetValue(guid, out testDownload))
                {
                    downloadsToMove.Add(testDownload);
                }
            }

            Dictionary<string, IDownload> sortedDownloadsToMove = downloadsToMove.OrderBy(d => d.Position).ToDictionary(download => download.GUID);

            //
            // Start operaton
            //
            TaskCompletionSource<QueueOperation> taskCompletionSource = new TaskCompletionSource<QueueOperation>();
            StartOperation(taskCompletionSource, downloadsToMove.Count);

            if (taskCompletionSource.Task.IsFaulted)
            {
                return taskCompletionSource.Task;
            }

            //
            // If there are no downloads to move end operation
            //
            if (downloadsToMove.Count == 0)
            {
                EndOperation(taskCompletionSource);
                return taskCompletionSource.Task;
            }

            //
            // Sort downloads into a list
            //
            List<IDownload> sortedDownloads = Downloads.Values.OrderBy(d => d.Position).ToList();

            //
            // Move downloads
            //
            foreach (IDownload download in sortedDownloadsToMove.Values)
            {
                //
                // Get downloads index in the sortedDownloads collection 
                //
                int i = sortedDownloads.IndexOf(download);

                //
                // Continue to next iteration if element is already at the bottom or wasn't found
                //
                if (i < 0 || i >= sortedDownloads.Count -1)
                {
                    continue;
                }

                //
                // Swap indexes and position properties with element below the current element
                // if the element below the current element is an element that also needs to be
                // moved Down then move to next iteration
                //
                IDownload lowerDownload = sortedDownloads[i + 1];

                IDownload testDownload;
                if (sortedDownloadsToMove.TryGetValue(lowerDownload.GUID, out testDownload))
                {
                    continue;
                }

                sortedDownloads[i + 1] = download;
                sortedDownloads[i] = lowerDownload;

                int lowerPosition = lowerDownload.Position;
                lowerDownload.Position = download.Position;
                download.Position = lowerPosition;

                //
                // Update operation for current download
                //
                UpdateOperation(taskCompletionSource, download, null, false, false);

                if (taskCompletionSource.Task.IsFaulted)
                {
                    return taskCompletionSource.Task;
                }
            }

            return taskCompletionSource.Task;
        }

        /// <summary>
        /// Checks if a download is running
        /// </summary>
        /// <param name="download">The download to check.</param>
        /// <returns>True if the download is running otherwise false.</returns>
        public bool IsDownloadRunning(IDownload download)
        {
            return IsDownloadRunning(download.GUID);
        }

        /// <summary>
        /// Checks if a download is running
        /// </summary>
        /// <param name="guid">The GUID of the download to check.</param>
        /// <returns>True if the download is running otherwise false.</returns>
        public bool IsDownloadRunning(string guid)
        {
            //
            // Get if the download is running
            //
            DownloadProcessor downloadProcessor;
            return RunningDownloads.TryGetValue(guid, out downloadProcessor);
        }

        #endregion Method

        #region Private Methods

        /// <summary>
        /// Adds a download to the queue. If the downloads status is Downloading,
        /// PausePending, StopPending or CancelPending then it's updated to
        /// Queued, Paused, Stopped or Canceled. This is done because none of those
        /// statuses are valid for a download being added to a queue and are probably
        /// due to an inproper shutdown. Note that is ONLY adds to the queue, it doesn't
        /// process the StartDownloadsImmediately property.
        /// </summary>
        /// <param name="download">The download to be added to the queue</param>
        private void AddDownloadToQueue(IDownload download)
        {
            //
            // Check if the download exists in the queue 
            //
            IDownload testDownload;
            if (Downloads.TryGetValue(download.GUID, out testDownload))
            {
                return;
            }

            //
            // Fix info on the data that has already been downloaded
            //
            download.ResetDownloadedInfo(download.Status == DownloadStatuses.Finished);

            //
            // Fix any invalid statuses
            //
            download.FixStatus(DownloadStatuses.Ready, DownloadStatuses.Ready, DownloadStatuses.Ready);

            //
            // Add to the queue
            //
            Downloads.Add(download.GUID, download);
            download.Position = Downloads.Count;
        }

        /// <summary>
        /// Stops the given download to a queued status.
        /// </summary>
        /// <param name="download">The download to stop.</param>
        private void StopDownloadToQueued(IDownload download)
        {
            StopDownloadToQueued(download.GUID);
        }

        /// <summary>
        /// Stops the given download to a queued status.
        /// </summary>
        /// <param name="guid">The GUID of the download to stop.</param>
        private void StopDownloadToQueued(string guid)
        {
            //
            // Get download from GUID
            //
            IDownload download;
            if (!Downloads.TryGetValue(guid, out download))
            {
                throw new DownloadNotFoundException("Download not found in the queue.");
            }

            //
            // Check if the download is running
            //
            DownloadProcessor processor;
            if (!RunningDownloads.TryGetValue(guid, out processor))
            {
                //
                // Check the download is in a valid state
                //
                if (download.Status != DownloadStatuses.Paused && download.Status != DownloadStatuses.Downloading && download.Status != DownloadStatuses.Queued)
                {
                    throw new InvalidStatusException("Invalid status, download must be Paused, Queued or Downloading.");
                }

                //
                // Update the status of the download to be queued.
                //
                download.Status = DownloadStatuses.Queued;

                //
                // Raise Stopped event and exit method here because this is all 
                // that needs to be done if the download isn't being processed.
                //
                OnDownloadStopped(download, null, null, false, false);
                return;
            }

            //
            // Instruct the download processor to stop the download
            //
            processor.StopToQueue(null);
        }

        /// <summary>
        /// Subscribes to a downloa processors events
        /// </summary>
        /// <param name="downloadProcessor">The download processor who's events to subscribe to</param>
        private void SubscribeToDownloadProcessorEvents(DownloadProcessor downloadProcessor)
        {
            downloadProcessor.Finished -= DownloadProcessorFinished;
            downloadProcessor.Finished += DownloadProcessorFinished;

            downloadProcessor.Stopped -= DownloadProcessorStopped;
            downloadProcessor.Stopped += DownloadProcessorStopped;

            downloadProcessor.Paused -= DownloadProcessorPaused;
            downloadProcessor.Paused += DownloadProcessorPaused;

            downloadProcessor.Started -= DownloadProcessorStarted;
            downloadProcessor.Started += DownloadProcessorStarted;
        }

        /// <summary>
        /// Tries to unsubscribe from a download processors events for a given download GUID
        /// </summary>
        /// <param name="guid">The GUID of the download who's download processor's events to unsubscribe from</param>
        private void UnsubscribeFromDownloadProcessorEvents(string guid)
        {
            DownloadProcessor processor;
            if (RunningDownloads.TryGetValue(guid, out processor))
            {
                UnsubscribeFromDownloadProcessorEvents(processor);
            }
        }

        /// <summary>
        /// Unsubscribes from a download processors events
        /// </summary>
        /// <param name="downloadProcessor">The download processor who's events to unsubscribe from</param>
        private void UnsubscribeFromDownloadProcessorEvents(DownloadProcessor downloadProcessor)
        {
            downloadProcessor.Finished -= DownloadProcessorFinished;
            downloadProcessor.Stopped -= DownloadProcessorStopped;
            downloadProcessor.Paused -= DownloadProcessorPaused;
            downloadProcessor.Started -= DownloadProcessorStarted;
        }

        /// <summary>
        /// Updates the maximum number of retries on all running downloads from 
        /// the queue's current value.
        /// </summary>
        private void UpdateMaxRetries()
        {
            //
            // Update all the download processors
            //
            foreach (DownloadProcessor downloadProcessor in RunningDownloads.Values)
            {
                downloadProcessor.MaxRetries = MaxRetries;
            }
        }

        /// <summary>
        /// Updates the maximum number of threads on all running downloads
        /// from the queue's current value.
        /// </summary>
        private void UpdateMaxThreads()
        {
            //
            // Update all the download processors
            //
            foreach (DownloadProcessor downloadProcessor in RunningDownloads.Values)
            {
                downloadProcessor.MaxThreads = MaxThreads;
            }
        }

        /// <summary>
        /// Updates the bandwidth providers of downloads in the current queue.
        /// </summary>
        private void UpdateBandwidthProviders()
        {
            //
            // Update all the download processors
            //
            foreach (DownloadProcessor downloadProcessor in RunningDownloads.Values)
            {
                downloadProcessor.BandwidthProvider = BandwidthProvider;
            }
        }

        /// <summary>
        /// Ensures that the correct number of downloads are running and that they are 
        /// are running in the correct sequence. This method only acts upon downloads 
        /// which are running or which are ready to start.
        /// </summary>
        private void SequenceQueuedDownloads()
        {
            //
            // Get the download that should be running.
            //
            IEnumerable<IDownload> validDownloads = (from d in Downloads.Values
                                                     where d.Status == DownloadStatuses.Downloading || d.Status == DownloadStatuses.Queued
                                                     orderby d.Position
                                                     select d).Take(MaxRunningDownloads);

            //
            // Get downloads that are running that shouldn't be
            //
            IEnumerable<IDownload> invalidDownloads = from d in RunningDownloads.Values
                                                      where !validDownloads.Contains(d.Download)
                                                      select d.Download;

            //
            // Stop & queue downloads that shouldn't be running
            //
            foreach (IDownload download in invalidDownloads)
            {
                StopDownloadToQueued(download);
            }
        }

        /// <summary>
        /// Starts a new operation.
        /// </summary>
        /// <param name="taskCompletionSource">The completion source for the task of the current operation.</param>
        /// <param name="actionCount">The number of actions in the operation</param>
        private void StartOperation(TaskCompletionSource<QueueOperation> taskCompletionSource, int actionCount)
        {
            //
            // If task completion source is null nothing
            //
            if (taskCompletionSource == null)
            {
                return;
            }

            //
            // Check and operation is already pending
            //
            QueueOperation operation;
            if (Operations.TryGetValue(taskCompletionSource, out operation))
            {
                taskCompletionSource.SetException(new OperationPendingException("Operation already pending for given state."));
                return;
            }

            //
            // Create new operation and add to operation list
            //
            operation = new QueueOperation(actionCount);
            Operations.Add(taskCompletionSource, operation);
        }

        /// <summary>
        /// Updates an operation with an action performed on a download.
        /// </summary>
        /// <param name="taskCompletionSource">The completion source for the task of the current operation.</param>
        /// <param name="download">The download the action was performed on.</param>
        /// <param name="error">Any error that may have occured during the action.</param>
        /// <param name="canceled">Indicates is the action was canceled.</param>
        /// <param name="aborted">Indicates if the action was aborted.</param>
        private void UpdateOperation(TaskCompletionSource<QueueOperation> taskCompletionSource, IDownload download, Exception error, bool canceled, bool aborted)
        {
            //
            // If task completion source is null do nothing
            //
            if (taskCompletionSource == null)
            {
                return;
            }

            lock (_operationSyncObject)
            {
                //
                // Get the operation for the task completion source
                //
                QueueOperation operation;
                if (!Operations.TryGetValue(taskCompletionSource, out operation))
                {
                    taskCompletionSource.SetException(new OperationNotFoundException("Operation not found for given state."));
                    return;
                }
                
                //
                // Increment action completed count
                //
                operation.ActionCompletedCount++;

                //
                // Add download to operation
                //
                operation.Downloads.Add(download);

                //
                // Add error info
                //
                Exception ex;
                if (operation.DownloadErrors.TryGetValue(download, out ex))
                {
                    taskCompletionSource.SetException(new Exception("Operation already has a recorded error for download."));
                    return;
                }
                
                operation.DownloadErrors.Add(download, error);

                //
                // Add cancel info
                //
                bool cn;
                if (operation.DownloadCancels.TryGetValue(download, out cn))
                {
                    taskCompletionSource.SetException(new Exception("Operation already has recorded cancel info for download."));
                    return;
                }
                
                operation.DownloadCancels.Add(download, canceled);

                //
                // Add abort info
                //
                bool ab;
                if (operation.DownloadAborts.TryGetValue(download, out ab))
                {
                    taskCompletionSource.SetException(new Exception("Operation already has recorded abort info for download."));
                    return;
                }
                
                operation.DownloadAborts.Add(download, aborted);

                //
                // End operation if all actions have been performed
                //
                if (operation.ActionCount == operation.ActionCompletedCount)
                {
                    EndOperation(taskCompletionSource);
                }
            }
        }

        /// <summary>
        /// Updates an operation with a performed action.
        /// </summary>
        /// <param name="taskCompletionSource">The completion source for the task of the current operation.</param>
        private void UpdateOperation(TaskCompletionSource<QueueOperation> taskCompletionSource)
        {
            //
            // If task completion source is null do nothing
            //
            if (taskCompletionSource == null)
            {
                return;
            }

            lock (_operationSyncObject)
            {
                //
                // Get the operation for the task completion source
                //
                QueueOperation operation;
                if (!Operations.TryGetValue(taskCompletionSource, out operation))
                {
                    taskCompletionSource.SetException(new OperationNotFoundException("Operation not found for given state."));
                    return;
                }
                
                //
                // Increment action completed count
                //
                operation.ActionCompletedCount++;

                //
                // End operation if all actions have been performed
                //
                if (operation.ActionCompletedCount >= operation.ActionCount)
                {
                    EndOperation(taskCompletionSource);
                }
            }
        }

        /// <summary>
        /// Ends an operation.
        /// </summary>
        /// <param name="taskCompletionSource">The completion source for the task of the current operation.</param>
        private void EndOperation(TaskCompletionSource<QueueOperation> taskCompletionSource)
        {
            EndOperation(taskCompletionSource, null);
        }

        /// <summary>
        /// Ends an operation.
        /// </summary>
        /// <param name="taskCompletionSource">The completion source for the task of the current operation.</param>
        /// <param name="error">The error to set for the task.</param>
        private void EndOperation(TaskCompletionSource<QueueOperation> taskCompletionSource, Exception error)
        {
            //
            // If task completion source is null do nothing
            //
            if (taskCompletionSource == null)
            {
                return;
            }

            //
            // Get the operation for the task completion source
            //
            QueueOperation operation;
            if (!Operations.TryGetValue(taskCompletionSource, out operation))
            {
                taskCompletionSource.SetException(new OperationNotFoundException("Operation not found for given state."));
                return;
            }

            //
            // Set result/error
            //
            if (error != null)
            {
                taskCompletionSource.SetException(error);
            }
            else
            {
                taskCompletionSource.SetResult(operation);
            }

            //
            // Remove operation form operation list
            //
            Operations.Remove(taskCompletionSource);
        }

        #endregion Private Methods

        #region Download Event Handlers

        /// <summary>
        /// Handles when a download processor finished a download
        /// </summary>
        private void DownloadProcessorFinished(object sender, DownloadOperationCompletedEventArgs e)
        {
            //
            // Unsubscribe from events
            //
            UnsubscribeFromDownloadProcessorEvents(e.Download.GUID);
            
            //
            // Remove the the download from running downloads
            //
            RunningDownloads.Remove(e.Download.GUID);

            //
            // Start queued downloads
            //
            StartQueuedDownloads();

            //
            // Raise DownloadFinished event
            //
            OnDownloadFinished(e.Download, e.StateObject);

            //
            // Update operation
            //
            TaskCompletionSource<QueueOperation> taskCompletionSource = e.StateObject as TaskCompletionSource<QueueOperation>;
            UpdateOperation(taskCompletionSource, e.Download, null, false, false);
        }

        /// <summary>
        /// Handles when a download processor stops a download
        /// </summary>
        private void DownloadProcessorStopped(object sender, DownloadOperationCompletedEventArgs e)
        {
            //
            // Unsubscribe from events
            //
            UnsubscribeFromDownloadProcessorEvents(e.Download.GUID);

            //
            // Remove the the download from running downloads
            //
            RunningDownloads.Remove(e.Download.GUID);

            //
            // Start queued downloads
            //
            StartQueuedDownloads();

            //
            // Raise DownloadStopped event
            //
            OnDownloadStopped(e.Download, e.StateObject, e.Error, e.Aborted, e.Canceled);

            //
            // Update operation
            //
            TaskCompletionSource<QueueOperation> taskCompletionSource = e.StateObject as TaskCompletionSource<QueueOperation>;
            UpdateOperation(taskCompletionSource, e.Download, e.Error, e.Canceled, e.Aborted);
        }

        /// <summary>
        /// Handles when a download processor pauses a download
        /// </summary>
        private void DownloadProcessorPaused(object sender, DownloadOperationCompletedEventArgs e)
        {
            //
            // Unsubscribe from events
            //
            UnsubscribeFromDownloadProcessorEvents(e.Download.GUID);

            //
            // Remove the the download from running downloads
            //
            RunningDownloads.Remove(e.Download.GUID);

            //
            // Start queued downloads
            //
            StartQueuedDownloads();

            //
            // Raise DownloadPaused event
            //
            OnDownloadPaused(e.Download, e.StateObject);

            //
            // Update operation
            //
            TaskCompletionSource<QueueOperation> taskCompletionSource = e.StateObject as TaskCompletionSource<QueueOperation>;
            UpdateOperation(taskCompletionSource, e.Download, null, false, false);
        }

        /// <summary>
        /// Handles when a download processor starts a download
        /// </summary>
        private void DownloadProcessorStarted(object sender, DownloadOperationCompletedEventArgs e)
        {
            //
            // Raise DownloadPaused event
            //
            OnDownloadStarted(e.Download, e.StateObject);

            //
            // Update operation
            //
            TaskCompletionSource<QueueOperation> taskCompletionSource = e.StateObject as TaskCompletionSource<QueueOperation>;
            UpdateOperation(taskCompletionSource, e.Download, null, false, false);
        }

        #endregion Download Event Handlers
    }
}
